<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>我的第一个WebRTC聊天室</title>
  <style>
    .top {
      vertical-align: top
    }

    .input {
      border: solid 1px gray;
      min-height: 1.2em;
      min-width: 25em;
      width: 100%;
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
  </style>
</head>

<body>
  <p>我的第一个WebRTC聊天室</p>
  <p>使用方法: 在同一个浏览器打开两个本页面,然后分别点连接按钮即可发送消息</p>
  <table>
    <tr>
      <th>名称</th>
      <th>内容</th>
      <th>操作</th>
    </tr>
    <tr>
      <td><label>name</label></td>
      <td><input title="name" id="myName" placeholder="请输入昵称" class="input"></td>
      <td>
        <button title="connect" id="myBtnConnect">连接</button>
      </td>
    </tr>
    <tr>
      <td><label>msg</label></td>
      <td><input title="name" id="myMsg" placeholder="请输入消息内容" class="input"></td>
      <td>
        <button title="send" id="myBtnSend">发送</button>
      </td>
    </tr>
    <tr>
      <td class="top"><label>msg history</label></td>
      <td>
        <div title="msgHistory" id="myMsgHistory" class="input">
          <div>=========聊天记录==========</div>
        </div>
      </td>
      <td class="top">
        <button title="send" id="myBtnClear">清空</button>
      </td>
    </tr>
  </table>
</body>


<script>

  // 常量信息
  const Constant = {
    BTN_UN_CONNECT: '连接',
    BTN_CONNECTED: '已连接',
    BTN_CONNECTING: '连接中',
    BTN_CONNECT_FAILD: '连接失败',
  };
  // 配置最大同时在线聊天数量
  const maxUsers = 8;
  // 配置ice信息
  let config = {
    "iceServers": [
      { "urls": ["stun:b.7mo.org:3478"] }
    ],
    "iceTransportPolicy": "all",
    "iceCandidatePoolSize": "0"
  };

  //=========函数封装
  //上次消息事件记录
  let last = {
    //远程创建数据通道事件
    eventOnDataChannel: {},
    //远程创建数据通道后打开通道事件
    eventOnOpenForDataChannel: {},
    //ice候选地址事件
    iceEvent: {},
    //ice候选地址状态改变事件
    eventOnIceGatheringStateChange: {},
    //ice候选地址错序事件
    iceErrorEvent: {},
    // 候选地址记录集
    candidates: [],
    // 候选地址记录
    candidate: {},
    // 数据通道传输记录
    chats: [],
    // 数据通道记录
    channels: [],
    // ice配置信息
    config: {},
  };

  //------------基础通用方法
  // 对象工具类
  let Utils = (() => {
    // 判断对象是否为空
    let isEmpty = (obj) => obj === undefined || !obj;
    //自定义随机id字段
    let randomId = () => Math.random().toString().substring(2, 18);
    return {
      name: 'Utils',
      isEmpty: isEmpty,
      randomId: randomId,
    }
  })();

  //根据用户uid获取用户信息
  let getUserByUid = (uid) => {
    /*
    用户信息
    */
    let user = null;
    let i = 0;
    while (i < maxUsers) {
      let key = 'pcUser0' + i;
      user = JSON.parse(window.localStorage.getItem(key));
      if (user == null) {
        if (uid == null || uid.toString().length === 0) {
          uid = Utils.randomId();
        }
        user = {
          // 用户id,作为全网唯一id
          uid: uid,
          // pc的ic,每次pc创建后会更新此id
          pid: 0,
          // 用户名称
          uname: undefined,
          sdpLocal: {},
          // 请求方sdp信息
          osdp: {},
          // 响应方sdp信息
          rsdp: {},
        };
        eleMyMsgHistory.say('新建用户', user);
        break;
      } else {
        if (uid === user.uid) {
          break;
        }
      }
    }
    if (user == null) {
      eleMyMsgHistory.say('在线用于已满 %s 人,无法创建用户', maxUsers);
    }
    return user;
  };

  // 更新用户信息持久化
  let updateUserByUid = (user) => {
    if (user === null || !user.uid) {
      eleMyMsgHistory.say('保存用户信息失败,空的用户信息!', user)
    } else {
      localStorage.setItem(user.uid, JSON.stringify(user));
    }
  };

  // ------------------- WebRtc基础方法
  //简单工厂模式
  let RtcFactory = (() => {
    // 初始化状态
    const InitState = {
      // 步骤0: 未做任何初始化
      NONE: 'none',
      // 步骤1: 已初始化用户信息
      USER: 'user',
      // 步骤2: 已初始化ice
      ICE: 'ice',
      // 步骤3: 已初始化本地
      LOCAL: 'local',
      // 步骤4: 已初始化远程
      REMOTE: 'remote',
      // 步骤5: 已建立连接
      CONNECTED: 'connected',
      // 步骤9: 已损坏, 表示无法再次使用了.需要重新获取一个新的pc
      BROKEN: 'broken',
    };
    //pc类型: 本地, 远程
    const PcType = {
      // offer端,作为发起端
      LOCAL: 'local',
      // answer端,作为应答端
      REMOTE: 'remote',
    };

    // offer选项
    const offerOption = { offerToReceiveAudio: true, };

    // ice事件候选地址消息
    let onIceCandidate = (iceEvent) => {
      last.iceEvent = iceEvent;
      console.log('iceEvent %o', iceEvent);
      let pc = iceEvent.target;
      iceEvent.parent = pc;
      if (iceEvent.candidate) {
        if (iceEvent.candidate.candidate === '') {
          return;
        }
        let { candidate } = iceEvent;
        console.log('iceEvent.candidate', candidate);
        eleMyMsgHistory.say('获取候选地址信息' + pc.iceGatheringState + JSON.stringify(candidate));
        last.candidates.push(candidate);
        last.candidate = candidate;
      } else if (!('onicegatheringstatechange' in RTCPeerConnection.prototype)) {
        // 判断是否支持此功能
        // should not be done if its done in the icegatheringstatechange callback.
        console.log('should not be done if its done in the icegatheringstatechange callback.');
        pc.close();
        pc = null;
      } else if (iceEvent.candidate == null) {
        //ice候选处理完成
        pc.InitState = InitState.ICE;
        if (pc.pcType === PcType.LOCAL) {
          console.log('ice候选请求地址处理完毕.', pc.pid, pc.localDescription);
        }
        if (pc.pcType === PcType.REMOTE) {
          pc.rsdp = pc.localDescription;
          pc.user.rsdp = pc.localDescription;
          console.log('ice候选响应地址处理完毕.', pc.pid, pc.localDescription);
          updateUserByUid(pc.user);
        }
      }
    };

    // ICE候选地址失败
    let onIceCandidateError = (iceErrorEvent) => {
      last.iceErrorEvent = iceErrorEvent;
      console.log('iceErrorEvent', iceErrorEvent);
      eleMyMsgHistory.say('ICE候选地址失败!' + JSON.stringify(iceErrorEvent));
    };

    // 消息方法命名规范: 如遇到容易混淆的方法,在方法后添加ForXxx对象来方便识别方法用途
    // 远程数据通道打开事件 (远程通过创建数据通道后,先经由onDataChannel,再经过本方法onOpen)
    // 本方法仅能获取一些dataChannel的属性,具体什么用尚未研究
    let onOpenForDataChannel = (eventOnOpenForDataChannel) => {
      last.eventOnOpenForDataChannel = eventOnOpenForDataChannel;
      console.log('eventOnOpenForDataChannel', eventOnOpenForDataChannel);
      let dataChannel = eventOnOpenForDataChannel.target;
      // 遵循所有pc派生出来的对象都应设置父级对象为pc本身,以让所有派生对象能方便拿到原始pc对象
      eventOnOpenForDataChannel.parent = dataChannel.parent;
    };

    // 远程数据通道创建事件
    let onDataChannel = (eventOnDataChannel) => {
      last.eventOnDataChannel = eventOnDataChannel;
      console.log('eventOnDataChannel', eventOnDataChannel);
      // 接收数据通道的父级pc
      let pc = eventOnDataChannel.target;
      let { channel } = eventOnDataChannel;
      // 遵循所有pc派生出来的对象都应设置父级对象为pc本身,以让所有派生对象能方便拿到原始pc对象
      channel.parent = pc;
      channel.onopen = onOpenForDataChannel;
      last.channels.push(eventOnDataChannel.channel);
    };

    // ICE候选者状态改变事件
    let onIceGatheringStateChange = (eventOnIceGatheringStateChange) => {
      eleMyMsgHistory.say('ICE候选者状态改变事件', eventOnIceGatheringStateChange);
      let pc = eventOnIceGatheringStateChange.target;
      eventOnIceGatheringStateChange.parent = pc;
      last.eventOnIceGatheringStateChange = eventOnIceGatheringStateChange;
      if (!eventOnIceGatheringStateChange.target && eventOnIceGatheringStateChange.target.iceGatheringState === 'complete') {
        eleMyMsgHistory.say('ICE候选者状态完成!');
        pc.initState = InitState.ICE;
        let user = pc.user;
        user.sdpLocal = pc.localDescription;
      }
    };

    // 初始化方法
    let initFirst = (uname) => {
      if (Utils.isEmpty(uname)) {
        console.log('初始化pc失败,未指定用户uname', pc);
        return false;
      }
      // 初始化用户信息
      let user = getUserByUid(pc.uid);
      if (user == null) {
        eleMyMsgHistory.say('初始化pc失败! 用户已满');
        throw '初始化pc失败! 用户已满';
      } else {
        user.uname = uname;
        user.pid = pc.pid;
        pc.user = user;
        pc.uid = user.uid;
        updateUserByUid(user);

        // 设置ice事件候选地址消息
        pc.onicecandidate = onIceCandidate;
        // 设置ice候选地址状态改变消息处理
        pc.onicegatheringstatechange = onIceGatheringStateChange;
        // 设置ice候选地址错误事件
        pc.onicecandidateerror = onIceCandidateError;
        // 指定远程建立通道通知事件处理
        pc.ondatachannel = onDataChannel;
      }
      console.log('初始化pc', pc);
      return true;
    };

    // 初始化本地发起连接
    let initLocal = async () => {
      if (pc.initState === InitState.ICE) {
        let offer = await pc.createOffer(offerOption);
        console.log('ice已初始化offer', offer);
        pc.osdp = offer;
        pc.user.osdp = offer;
        console.log('=============== 设置offer ===========');
        pc.initState = InitState.LOCAL;
        updateUserByUid(pc.user);
        return true;
      } else {
        eleMyMsgHistory.say('ice尚未初始化');
      }
      return false;
    };

    // 初始化远程收到连接响应
    let initRemote = async (osdp) => {
      if (pc.initState === InitState.ICE) {
        if (!osdp) {
          console.log('未指定osdp');
          return false;
        }
        pc.setRemoteDescription(osdp);
        let answer = await pc.createAnswer();
        console.log('ice已初始化answer', answer);
        pc.initState = InitState.REMOTE;
        return true;
      } else {
        eleMyMsgHistory.say('ice尚未初始化');
      }
      return false;
    };

    let initConnect = async () => {
      let initConnectResult = false;
      if (pc.pcType === 'local') {
        initConnectResult = initLocal();
      }
      if (pc.pcType === 'remote') {
        initConnectResult = initRemote();
      }
      return initConnectResult;
    };

    // 创建pc
    let createPeerConnection = (config) => {
      if (Utils.isEmpty(config)) {
        console.log('config 未指定');
        return undefined;
      }
      last.config = config;
      let pc = new RTCPeerConnection(config);
      /*对pc进行自定义字段及方法*/
      // 自定义随机uid字段
      pc.pid = Utils.randomId();
      //用户id
      pc.uid = '';
      // 自定义用户信息字段
      pc.user = {};
      // 自定义channels数组字段
      // 数据通道
      pc.channels = [];
      // 自定义父级对象是他自己
      // 父级对象,pc派生出来的所有对象都应该设置父级对象为pc,这样就能方便获取每一个派生对象的父级pc对象
      pc.parent = pc;
      // 初始化sdp信息
      pc.osdp = null;
      // 初始化sdp信息
      pc.rsdp = null;
      // pc类型,LOCAL,REMOTE,默认值=local;
      pc.pcType = PcType.LOCAL;
      // 初始化状态
      pc.initState = InitState.NONE;
      // 自定义init初始化对等连接建立方法
      // 定义初始化pc方法
      pc.initFirst = initFirst;
      pc.initConnect = initConnect;
      return pc;
    };
    return { createPeerConnection: createPeerConnection }
  })();

  //=========函数封装完成------------------

  // 创建连接对等点
  let pc = RtcFactory.createPeerConnection(config);

  //初始化元素对象
  let eleMyName = document.getElementById('myName');
  let eleMyMsg = document.getElementById('myMsg');
  let eleBtnConnect = document.getElementById('myBtnConnect');
  let eleBtnSend = document.getElementById('myBtnSend');
  let eleBtnClear = document.getElementById('myBtnClear');
  let eleMyMsgHistory = document.getElementById('myMsgHistory');

  //初始化元素方法
  eleMyMsgHistory.say = (msg) => {
    let div = document.createElement('div');
    div.class = 'input';
    div.innerText = '[' + new Date().toLocaleTimeString() + ']\t' + msg;
    eleMyMsgHistory.appendChild(div);
  };

  eleMyMsgHistory.clear = () => {
    let children = eleMyMsgHistory.children;
    while (children.length > 1) {
      eleMyMsgHistory.removeChild(children.item(1));
    }
  };

  //定义初始化按钮事件
  let connect = (e) => {
    console.log('点击连接按钮', e);
    last.e = e;
    let uname = eleMyName.value;
    console.log('uname', !uname);
    if (!uname) {
      console.log('用户名不能为空');
      eleMyMsgHistory.say('用户名不能为空');
      eleMyName.focus();
      return;
    }
    // initPeerConnection(uname);
    // todo 初始化建立连接
    console.log('初始化建立连接');
    let initResult = pc.initFirst(uname);
    if (initResult) {
      let initConnectResult = pc.initConnect();
      if (initConnectResult) {

      } else {
        console.log('初始化建立连接失败')
      }
    } else {
      console.log('初始化连接失败')
    }
  };
  let send = (e) => {
    console.log('点击发送按钮', e);
    window.e = e;
    let btnConnectName = eleBtnConnect.innerText;
    console.log('btnConnectName', btnConnectName);
    if (btnConnectName === Constant.BTN_UN_CONNECT) {
      console.log('对等点尚未连接');
      eleMyMsgHistory.say('对等点尚未连接,请点击[连接]按钮');
      eleBtnConnect.focus();
      return;
    }
    let msg = eleMyMsg.value;
    if (!msg) {
      console.log('消息内容不能为空');
      eleMyMsgHistory.say('消息内容不能为空');
      eleMyMsg.focus();
      return;
    }
    console.log('发送消息:', msg);
  };
  let clear = (e) => {
    console.log('点击清空按钮', e);
    eleMyMsgHistory.clear();
  };

  //初始化元素事件
  eleBtnConnect.onclick = connect.bind(eleBtnConnect);
  eleBtnSend.onclick = send.bind(eleBtnSend);
  eleBtnClear.onclick = clear.bind(eleBtnClear);


</script>

</html>